package game.game;

import com.crunch.core.Actor;
import com.crunch.core.GameState;
import com.crunch.core.Input;
import com.crunch.core.Renderable;
import com.crunch.graphics.gpu.GpuBlendState;
import com.crunch.graphics.gpu.GpuState;
import com.crunch.physics.PhysicsSimulation;
import com.crunch.physics.Segment;
import game.CoreState;
import game.game.editors.ActorEditor;
import game.game.editors.BackgroundEditor;
import game.game.editors.PhysicsEditor;
import game.game.editors.TileEditor;
import org.json.JSONException;
import scenery.BackgroundLayerInstance;
import scenery.SceneryRenderer;
import scenery.TileLayerInstance;
import util.Camera;
import util.PhysicsDebugRenderer;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

// state for a playable room
public class RoomState extends GameState implements Renderable {
	public static final float PHYSICS_DT        = 1.0f / 60.0f;
	public static final float PHYSICS_SCALE     = 16.0f;

	public RoomState() {
		super(0);
	}

	@Override
	public void onCreate(Object... args) {
		coreState = (CoreState) args[0];

		firstTick = true;
		ticks = 0;

		camera = createActor(Camera.class);
		camera.setViewport(0, 0, coreState.getGameWidth(), coreState.getGameHeight());
		camera.set((float) camera.getViewportWidth() * 0.5f, (float) camera.getViewportHeight() * 0.5f, 1.0f);

		guiCamera = createActor(Camera.class);
		guiCamera.setViewport(0, 0, coreState.getGameWidth(), coreState.getGameHeight());
		guiCamera.set((float) guiCamera.getViewportWidth() * 0.5f, (float) guiCamera.getViewportHeight() * 0.5f, 1.0f);

		// set up objects
		sceneryRenderer = new SceneryRenderer(coreState.resourceManager);

		backgroundLayers = new HashMap<BackgroundLayerInstance, Renderable>();
		tileLayers = new HashMap<TileLayerInstance, Renderable>();

		initializePhysicsSimulation();
		physicsDebugRenderer = new PhysicsDebugRenderer(coreState.resourceManager);
		createActor(PhysicsUpdater.class);

		physicsSimulating = true;
		drawPhysicsDebug = false;

		getEngine().window.renderer.register(this);

		gameActors = new HashSet<GameActor>();

		roomDefinition = new RoomDefinition();
		initializeFromDefinition();

		initializeEditors();
	}

	@Override
	public void onDestroy() {
		for (Renderable r : backgroundLayers.values()) {
			getEngine().window.renderer.unregister(r);
		}
		for (Renderable r : tileLayers.values()) {
			getEngine().window.renderer.unregister(r);
		}

		getEngine().window.renderer.unregister(this);

		terminateEditors();
	}

	@Override
	public void onUpdate() {
		if (firstTick) {
			firstTick = false;
		} else {
			++ticks;
		}

		switchEditors(editorIndex);
	}

	private void initializePhysicsSimulation() {
		PhysicsSimulation.Settings settings = new PhysicsSimulation.Settings();
		settings.contactThreshold = 0.001f;
		settings.gridCellSize = 16.0f;
		physicsSimulation = new PhysicsSimulation(settings);
	}

	public boolean initializeFromDefinition() {
		clearGameActorsForStateChange();

		roomWidth = roomDefinition.getRoomWidth();
		roomHeight = roomDefinition.getRoomHeight();

		// clear current state
		for (Renderable r : backgroundLayers.values()) {
			getEngine().window.renderer.unregister(r);
		}
		backgroundLayers.clear();
		for (Renderable r : tileLayers.values()) {
			getEngine().window.renderer.unregister(r);
		}
		tileLayers.clear();
		initializePhysicsSimulation();

		// create backgrounds/tiles/colliders
		for (int i = 0; i < roomDefinition.getBackgroundLayerCount(); ++i) {
			RoomDefinition.BackgroundLayerDefinition def = roomDefinition.getBackgroundLayer(i);
			BackgroundLayerInstance inst = new BackgroundLayerInstance(
					def.getBackgroundLayer(),
					coreState.resourceManager.getTexture(def.getTexture()),
					getEngine().window.renderer);
			addBackgroundLayer(inst);
		}

		for (int i = 0; i < roomDefinition.getTileLayerCount(); ++i) {
			RoomDefinition.TileLayerDefinition def = roomDefinition.getTileLayer(i);
			TileLayerInstance inst = new TileLayerInstance(
					def.getTileLayer(),
					coreState.resourceManager.getTexture(def.getTexture()),
					getEngine().window.renderer);
			addTileLayer(inst);
		}

		Segment[] segments = new Segment[roomDefinition.getStaticColliderCount()];
		for (int i = 0; i < roomDefinition.getStaticColliderCount(); ++i) {
			segments[i] = roomDefinition.getStaticCollider(i);
		}
		physicsSimulation.addStaticColliders(segments);

		// create actors
		for (int i = 0; i < roomDefinition.getActorCount(); ++i) {
			RoomDefinition.ActorDefinition actorDefinition = roomDefinition.getActor(i);

			// try to create class instance
			try {
				// find the class by name
				Class<?> actorClass = Class.forName(actorDefinition.getClassName());
				// verify that it is derived from GameActor
				if (!GameActor.class.isAssignableFrom(actorClass)) {
					throw new ClassNotFoundException("Invalid class " + actorDefinition.getClassName());
				}
				// cast its Class type to one derived from GameActor
				@SuppressWarnings("unchecked")
				Class<? extends GameActor> gameActorClass = (Class<? extends GameActor>) actorClass;
				// instantiate and call init
				GameActor gameActorInstance = createActor(gameActorClass);
				gameActorInstance.initState(actorDefinition.getState());
			} catch (ClassNotFoundException e) {
				System.out.println("Invalid class " + actorDefinition.getClassName());
				return false;
			} catch (JSONException e) {
				System.out.println("Failed to initialize state for class " + actorDefinition.getClassName());
				return false;
			}
		}

		return true;
	}

	private void clearGameActorsForStateChange() {
		// destroy all current objects
		HashSet<GameActor> copyGameActors = new HashSet<GameActor>();
		copyGameActors.addAll(gameActors);
		for (GameActor a : copyGameActors) {
			a.setDestroyedForStateChange();
			a.destroy();
		}
		// if destroy() calls destroy() of any other game actor, it will be destroyed twice and an exception will occur
		// make sure no new actors were created
		if (!gameActors.isEmpty()) {
			throw new IllegalStateException("Game actors created during state change");
		}
	}

	public CoreState getCoreState() {
		return coreState;
	}

	public Camera getCamera() {
		return camera;
	}

	public Camera getGuiCamera() {
		return guiCamera;
	}

	public int getRoomWidth() {
		return roomWidth;
	}

	public int getRoomHeight() {
		return roomHeight;
	}

	public static class PhysicsUpdater extends Actor {
		public PhysicsUpdater() {
			super(0); // update at time 0 - actors before physics should be negative, after physics should be positive
		}

		@Override
		public void onUpdate() {
			RoomState thisRoom = (RoomState) getGameState();
			if (thisRoom.physicsSimulating) {
				thisRoom.physicsSimulation.stepSimulation(PHYSICS_DT);
			}
		}
	}

	public void addBackgroundLayer(BackgroundLayerInstance backgroundLayer) {
		if (backgroundLayers.containsKey(backgroundLayer)) {
			return;
		}
		final BackgroundLayerInstance backgroundLayerF = backgroundLayer;
		Renderable r = new Renderable() {
			@Override
			public void render(GpuState gpuState) {
				GpuBlendState blendState = gpuState.createBlendState();
				blendState.setBlendFunction(GpuBlendState.BlendMode.ALPHA);
				blendState.bind();
				sceneryRenderer.drawBackgroundLayer(gpuState, camera, backgroundLayerF);
			}

			@Override
			public int getDepth() {
				return backgroundLayerF.getBackgroundLayer().getDepth();
			}
		};
		getEngine().window.renderer.register(r);
		backgroundLayers.put(backgroundLayer, r);
	}

	public void removeBackgroundLayer(BackgroundLayerInstance backgroundLayer) {
		Renderable r = backgroundLayers.remove(backgroundLayer);
		if (r == null) {
			return;
		}
		getEngine().window.renderer.unregister(r);
	}

	public Set<BackgroundLayerInstance> getBackgroundLayers() {
		return Collections.unmodifiableSet(backgroundLayers.keySet());
	}

	public void addTileLayer(TileLayerInstance tileLayer) {
		if (tileLayers.containsKey(tileLayer)) {
			return;
		}
		final TileLayerInstance tileLayerF = tileLayer;
		Renderable r = new Renderable() {
			@Override
			public void render(GpuState gpuState) {
				GpuBlendState blendState = gpuState.createBlendState();
				blendState.setBlendFunction(GpuBlendState.BlendMode.ALPHA);
				blendState.bind();
				blendState.bind();
				sceneryRenderer.drawTileLayer(gpuState, camera, tileLayerF, ticks);
			}

			@Override
			public int getDepth() {
				return tileLayerF.getTileLayer().getDepth();
			}
		};
		getEngine().window.renderer.register(r);
		tileLayers.put(tileLayer, r);
	}

	public void removeTileLayer(TileLayerInstance tileLayer) {
		Renderable r = tileLayers.remove(tileLayer);
		if (r == null) {
			return;
		}
		getEngine().window.renderer.unregister(r);
	}

	public Set<TileLayerInstance> getTileLayers() {
		return Collections.unmodifiableSet(tileLayers.keySet());
	}

	private RoomDefinition roomDefinition;

	public int getTicks() {
		return ticks;
	}

	public SceneryRenderer getSceneryRenderer() {
		return sceneryRenderer;
	}

	public PhysicsSimulation getPhysicsSimulation() {
		return physicsSimulation;
	}

	public PhysicsDebugRenderer getPhysicsDebugRenderer() {
		return physicsDebugRenderer;
	}

	private boolean firstTick;
	private int ticks;

	private CoreState coreState;

	private Camera camera;
	private Camera guiCamera;
	private int roomWidth, roomHeight;

	private SceneryRenderer sceneryRenderer;
	private HashMap<BackgroundLayerInstance, Renderable> backgroundLayers;
	private HashMap<TileLayerInstance, Renderable> tileLayers;

	private PhysicsSimulation physicsSimulation;
	private PhysicsDebugRenderer physicsDebugRenderer;

	private boolean physicsSimulating;
	private boolean drawPhysicsDebug;

	@Override
	public void render(GpuState gpuState) {
		if (drawPhysicsDebug) {
			physicsDebugRenderer.drawPhysicsSimulation(gpuState, camera, physicsSimulation, RoomState.PHYSICS_SCALE);
		}
	}

	@Override
	public int getDepth() {
		return Integer.MAX_VALUE - 10;
	}

	// GameActor tracking - package level access only for GameActor

	void addGameActor(GameActor gameActor) {
		gameActors.add(gameActor);
	}

	void removeGameActor(GameActor gameActor) {
		gameActors.remove(gameActor);
	}

	private HashSet<GameActor> gameActors;

	////////////////
	// EDITORS

	private Input.EventListener editorEventListener;
	private Actor currentEditor;
	private int editorIndex;
	private int currentEditorIndex;
	private boolean resetEditor;

	private void initializeEditors() {
		// add editors
		editorEventListener = new Input.EventListener() {
			@Override
			public void processEvent(AWTEvent e, int eventType) {
				if (eventType == Input.KEY_EVENT) {
					KeyEvent ev = (KeyEvent) e;
					if (ev.getID() == KeyEvent.KEY_PRESSED) {
						switch (ev.getKeyCode()) {
							case KeyEvent.VK_ESCAPE:
								editorIndex = -1;
								break;
							case KeyEvent.VK_F1:
								editorIndex = 0;
								break;
							case KeyEvent.VK_F2:
								editorIndex = 1;
								break;
							case KeyEvent.VK_F3:
								editorIndex = 2;
								break;
							case KeyEvent.VK_F4:
								editorIndex = 3;
								break;

							case KeyEvent.VK_ENTER:
								physicsSimulating = !physicsSimulating;
								break;

							case KeyEvent.VK_F8:
								if (editorIndex != 2) {
									drawPhysicsDebug = !drawPhysicsDebug;
								}
								break;

							// save/load
							case KeyEvent.VK_S:
								if (((KeyEvent) e).isControlDown() && currentEditorIndex >= 0) {
									try {
										roomDefinition.save(coreState.gameSettings.getRoomsDirectory().resolve("room.rm"));
									} catch (IOException e1) {
										System.out.println("Failed to save!");
									}
								}
								break;
							case KeyEvent.VK_L:
								if (((KeyEvent) e).isControlDown() && currentEditorIndex >= 0) {
									try {
										roomDefinition.load(coreState.gameSettings.getRoomsDirectory().resolve("room.rm"));
										initializeFromDefinition();
										resetEditor = true;
									} catch (IOException e1) {
										roomDefinition = new RoomDefinition(); // reset it
										initializeFromDefinition();
										System.out.println("Failed to load!");
									}
								}
								break;
						}
					}
				}
			}
		};
		editorIndex = currentEditorIndex = -1;
		editorEventListener.eventMask = Input.KEY_EVENT;
		getEngine().input.addEventListener(editorEventListener);
	}

	private void terminateEditors() {
		getEngine().input.removeEventListener(editorEventListener);
	}

	private void switchEditors(int i) {
		if (currentEditorIndex == i && !resetEditor) {
			return;
		}
		if (currentEditor != null) {
			currentEditor.destroy();
			currentEditor = null;
		}
		if (i < 0) {
			// start the game
			drawPhysicsDebug = false;
			if (!initializeFromDefinition()) {
				clearGameActorsForStateChange();
			} else {
				physicsSimulating = true;
			}
		} else {
			if (currentEditorIndex < 0) {
				// stop the game
				// easy way to do this: re-initialize the game and then clear the actors
				// (not the best way, but works fine for editing)
				initializeFromDefinition();
				clearGameActorsForStateChange();
				physicsSimulating = false;
			}
			switch (i) {
				case 0:
				{
					BackgroundEditor editor = createActor(BackgroundEditor.class);
					drawPhysicsDebug = false;
					editor.init(roomDefinition);
					currentEditor = editor;
					break;
				}
				case 1:
				{
					TileEditor editor = createActor(TileEditor.class);
					drawPhysicsDebug = false;
					editor.init(roomDefinition);
					currentEditor = editor;
					break;
				}
				case 2:
				{
					PhysicsEditor editor = createActor(PhysicsEditor.class);
					drawPhysicsDebug = true;
					editor.init(roomDefinition);
					currentEditor = editor;
					break;
				}
				case 3:
				{
					ActorEditor editor = createActor(ActorEditor.class);
					drawPhysicsDebug = false;
					editor.init(roomDefinition);
					currentEditor = editor;
					break;
				}
			}
		}
		currentEditorIndex = i;
		resetEditor = false;
	}
}
